筆記目錄

Skip to content

Entity Framework 中 DateTime 時區問題與解決方案

TLDR

  • DateTimeKind 屬性(Local, Utc, Unspecified)會影響 ToLocalTime()ToUniversalTime() 的轉換結果,若處理不當將導致時間偏差。
  • 從資料庫讀取 DateTime 時,因資料庫欄位不儲存時區資訊,EF Core 預設會將其 Kind 設為 Unspecified,導致前端顯示時間錯誤。
  • 解決方案應在資料存取層處理,而非在前端補上 Z 字元。
  • 推薦使用 EF Core 的 ValueConverter.NET 6ConfigureConventions() 全域設定,自動將讀取出的時間強制標記為 Utc

DateTime 的時區格式問題

當開發者不清楚 DateTime.Kind 的狀態時,直接呼叫 ToLocalTime()ToUniversalTime() 會產生非預期的時間偏移。

什麼情況下會遇到這個問題:當系統混用不同來源的 DateTime 物件,且未統一檢查或轉換 Kind 屬性時。

  • KindLocal:呼叫 ToLocalTime() 不會改變時間。
  • KindUtc:呼叫 ToUniversalTime() 不會改變時間。
  • KindUnspecified:系統會假設該時間為 UTC 並轉換為本機時間(增加偏移),或假設為本機時間並轉換為 UTC(減少偏移)。

為了避免此類問題,建議參考如 ABP.IO 等框架的做法,定義統一的 IClock 介面來標準化時間轉換,確保 Kind 符合預期。

Entity Framework 使用 DateTime 的時區問題

使用 datetimedatetime2 等不含時區的資料庫類型時,EF Core 取出的資料 Kind 預設為 Unspecified。這導致序列化給前端時,時間字串末尾缺少 Z 標記,進而引發前端顯示的時間與預期不符。

什麼情況下會遇到這個問題:使用 Code First 或反向工程產生 Entity,且資料庫欄位未包含時區資訊時。

解決方案:使用 ValueConverter 自動轉換

透過 ValueConverter,可以在資料寫入資料庫前確保為 UTC,並在讀取時強制指定 KindUtc

定義轉換器類別:

csharp
public class UtcDateTimeValueConverter : ValueConverter<DateTime, DateTime> {
    public UtcDateTimeValueConverter()
        : base(v => ToDb(v), v => FromDb(v)) {
    }

    private static DateTime ToDb(DateTime dateTime) {
        return dateTime.Kind == DateTimeKind.Utc ? dateTime : dateTime.ToUniversalTime();
    }

    private static DateTime FromDb(DateTime dateTime) {
        return DateTime.SpecifyKind(dateTime, DateTimeKind.Utc);
    }
}

全域套用設定

若要避免逐一設定屬性,建議在 DbContext 中使用 ConfigureConventions(.NET 6 以上版本)進行全域配置:

csharp
public partial class MyDbContext : DbContext {
    protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) {
        ArgumentNullException.ThrowIfNull(configurationBuilder);

        configurationBuilder.Properties<DateTime>().HaveConversion<UtcDateTimeValueConverter>();
    }
}

若無法使用 ConfigureConventions,則可透過 OnModelCreating 遍歷所有屬性並套用轉換器:

csharp
partial void OnModelCreatingPartial(ModelBuilder modelBuilder) {
    foreach (IMutableEntityType entityType in modelBuilder.Model.GetEntityTypes()) {
        foreach (IMutableProperty property in entityType.GetProperties()) {
            if (property.ClrType == typeof(DateTime) || property.ClrType == typeof(DateTime?)) {
                property.SetValueConverter(typeof(UtcDateTimeValueConverter));
            }
        }
    }
}

異動歷程

  • 2024-08-15 初版文件建立。